#include "src/scene/scene.h"

#include <cmath>
#include <GL/gl.h>
#include <iostream>
#include <sys/time.h>

#include "src/defines/rayState.h"
#include "src/defines/filterType.h"

#include "src/scene/combinedObject.h"
#include "src/scene/nearestInfo.h"
#include "src/scene/pointLight.h"
#include "src/scene/mask.h"
#include "src/scene/raytracing.h"
#include "src/scene/photonMapping.h"


Scene::Scene(Camera *_c){
    idObjects = 0;
    idLights = 0;
    backgroundColor = Color(0, 0, 0, 1);
    tree = NULL;
    if(_c) camera = _c;
    else camera = new Camera();
    raytracing = new Raytracing(this);
    photonMapping = new PhotonMapping(this);

    treeTime = raytracingTime = photonMappingTime = photonMappingPhotonsOnlyTime = emitPhotonsTime = 0;
}

Scene::~Scene(){
    delete tree;

    delete raytracing;
    delete photonMapping;

    unsigned int size = objects.size();
    for(unsigned int i = 0; i < size; i++){
        Object *obj = objects.back();
        objects.pop_back();
        delete obj;
    }
    delete camera;

    size = lights.size();
    for(unsigned int i = 0; i < size; i++){
        Light *light = lights.back();
        lights.pop_back();
        delete light;
    }
}


Raytracing *Scene::getRaytracing()
{
    return raytracing;
}

PhotonMapping *Scene::getPhotonMapping()
{
    return photonMapping;
}

void Scene::addLight(Light *_l){
    GLenum num = GL_LIGHT0 + lights.size() - 1;
    #if DEBUG
//    if(lights.empty()) std::cout <<"A priemira luz tem que ser ambiente!" << std::endl;
//    else
    if(lights.size() > 9) std::cout <<"Não pode ter mais de 8 luzes" << std::endl;
    #endif //DEBUG

    _l->shine(num);

    lights.push_back(_l);
//    std::cout << "tem " << lights.size() << "luzes " << std::endl;
}

void Scene::deleteLight(Light *_l){
    int size = lights.size();
    int i;
    for (i = size; i > 0; i--){
        if ( lights.at(i - 1) == _l ){
            lights.erase(lights.begin() + i - 1);
            break;
        }
    }

    i--;
    for(; i < size - 1; i++)
    {
        lights[i]->shine(GL_LIGHT0 + i - 1);
    }

    for(; i < 9; i++)
    {
        glDisable(GL_LIGHT0 + i - 1);
    }
}

void Scene::deleteObject(Object *_o){
    int size = objects.size();
    for (int i = size; i > 0; i--){
        if ( objects.at(i - 1) == _o ){
            objects.erase(objects.begin() + i - 1);
            break;
        }
    }
}

void Scene::draw(bool _showWireframeObjects, bool _showCamera, bool _showRayTest, bool _drawPhotons){

    if(_drawPhotons)
        photonMapping->drawPhotons();

    for(unsigned int i = 0; i < objects.size(); i++){
        objects[i]->draw(_showWireframeObjects);
    }
    if(_showCamera) camera->draw();

    for(unsigned int i = 0; i < lights.size(); i++){
        lights[i]->draw();
    }

    if(_showRayTest){
        drawRay();
    }

    if(tree) tree->draw();
}

void Scene::drawRay(){
//
//    Vector3D directionToLight = (light->getPosition() - nearestPoint).normalize();
//
//    double ln = nearestNormal.dotProduct(directionToLight);
//    Vector3D reflected = (_ray.getDirection() + (nearestNormal * (-2.0 * ln))).normalize();
//    Ray reflectionRay(nearestPoint + (reflected*0.01), reflected);

    if(!listOfPoints.empty()) {
        glPointSize(4);
        glDisable(GL_LIGHTING);

        Vector3D p1;
        Vector3D n1;
        Vector3D p2;
        unsigned int type;
        Trio trio;

        bool firstTime = true;
        for(unsigned int i = 0; i < listOfPoints.size() - 1; i++){
            glLineWidth(3);
            trio = listOfPoints.at(i);
            p1 = trio.point;
            n1 = trio.normal;
            type = trio.type;
            p2 = listOfPoints.at(i + 1).point;

            if(!firstTime){
                glColor3f(1.0, 1.0, 0.0);
                glBegin(GL_POINTS);
                    glVertex3d(p1.x, p1.y, p1.z);
                glEnd();
            }

            if(type == IN) glColor3f(1.0, 0.0, 0.0);
            else if (type == ON) glColor3f(1.0, 1.0, 0.0);
            else if (type == OUT) glColor3f(0.0, 1.0, 0.0);
            glBegin(GL_LINES);
                glVertex3d(p1.x, p1.y, p1.z);
                glVertex3d(p2.x, p2.y, p2.z);
            glEnd();

            if(firstTime){
                firstTime = false;
            }else{
                glLineWidth(5);
                glColor3f(0.0, 0.0, 1.0);
                glBegin(GL_LINES);
                    glVertex3d(p1.x, p1.y, p1.z);
                    Vector3D p1_ = p1 + (n1*(1.0/5.0));
                    glVertex3d(p1_.x, p1_.y, p1_.z);
                glEnd();
            }
        }

        glPointSize(1);
        glLineWidth(1);
        glEnable(GL_LIGHTING);
    }
    glColor3f(1.0, 1.0, 1.0);
}

void Scene::doHBBTree(){
    if(!tree){
        struct timeval t;
        gettimeofday(&t, NULL);
        double treeTimeStart = static_cast<double>(t.tv_sec) + static_cast<double>(t.tv_usec)*0.000001;


        tree = new HBBTree(objects, 0);

        gettimeofday(&t, NULL);
        double treeTimeEnd = static_cast<double>(t.tv_sec) + static_cast<double>(t.tv_usec)*0.000001;

        treeTime = treeTimeEnd - treeTimeStart;
        int days, hours, minutes, seconds;
        transformSecondToTime(&days, &hours, &minutes, &seconds, treeTime);
        std::cout << "\nDemorou " << days << " dias, " << hours << " horas, "
                  << minutes << " minutos " << seconds << " segundos para fazer a árvore. [" << treeTime << "]" << std::endl;

    }else std::cout << "Tree já existe!" <<std::endl;
}

void Scene::doRaytracing()
{
    if(objects.empty()) return;

    doHBBTree();


    struct timeval t;
    gettimeofday(&t, NULL);
    double raytracingTimeStart = static_cast<double>(t.tv_sec) + static_cast<double>(t.tv_usec)*0.000001;

    doRender(raytracing);

    gettimeofday(&t, NULL);
    double raytracingTimeEnd = static_cast<double>(t.tv_sec) + static_cast<double>(t.tv_usec)*0.000001;

    raytracingTime = raytracingTimeEnd - raytracingTimeStart;
    int days, hours, minutes, seconds;
    transformSecondToTime(&days, &hours, &minutes, &seconds, raytracingTime);
    std::cout << "\nDemorou " << days << " dias, " << hours << " horas, "
              << minutes << " minutos " << seconds << " segundos para fazer o rendering (raytracing). [" << raytracingTime<< "]" << std::endl;
}

void Scene::doEmitPhotons(bool _doItAgain)
{
    if(objects.empty()) return;

    doHBBTree();

    struct timeval t;
    gettimeofday(&t, NULL);
    double emitPhotonsTimeStart = static_cast<double>(t.tv_sec) + static_cast<double>(t.tv_usec)*0.000001;

    bool b = photonMapping->emitPhotons(_doItAgain);

    if(b)
    {
        gettimeofday(&t, NULL);
        double emitPhotonsTimeEnd = static_cast<double>(t.tv_sec) + static_cast<double>(t.tv_usec)*0.000001;

        emitPhotonsTime = emitPhotonsTimeEnd - emitPhotonsTimeStart;

        int days, hours, minutes, seconds;
        transformSecondToTime(&days, &hours, &minutes, &seconds, emitPhotonsTime);
        std::cout << "\nDemorou " << days << " dias, " << hours << " horas, "
                  << minutes << " minutos " << seconds << " segundos para fazer a emissão de photons. [" << emitPhotonsTime << "]" << std::endl;
    }
}

void Scene::doPhotonMapping(bool _onlyPhotons)
{
    doEmitPhotons();

    struct timeval t;
    gettimeofday(&t, NULL);
    double photonMappingTimeStart = static_cast<double>(t.tv_sec) + static_cast<double>(t.tv_usec)*0.000001;

    doRender(photonMapping, _onlyPhotons);

    gettimeofday(&t, NULL);
    double photonMappingTimeEnd = static_cast<double>(t.tv_sec) + static_cast<double>(t.tv_usec)*0.000001;

    if(_onlyPhotons)
    {
        photonMappingPhotonsOnlyTime = photonMappingTimeEnd - photonMappingTimeStart;

        int days, hours, minutes, seconds;
        transformSecondToTime(&days, &hours, &minutes, &seconds, photonMappingPhotonsOnlyTime);
        std::cout << "\nDemorou " << days << " dias, " << hours << " horas, "
                  << minutes << " minutos " << seconds << " segundos para fazer o rendering (somente dos photons). [" << photonMappingPhotonsOnlyTime<< "]" << std::endl;
    }
    else
    {
        photonMappingTime = photonMappingTimeEnd - photonMappingTimeStart;

        int days, hours, minutes, seconds;
        transformSecondToTime(&days, &hours, &minutes, &seconds, photonMappingTime);
        std::cout << "\nDemorou " << days << " dias, " << hours << " horas, "
                  << minutes << " minutos " << seconds << " segundos para fazer o rendering (photon mapping). [" << photonMappingTime<< "]" << std::endl;

    }
}

void Scene::doRender(Render *_render, bool _onlyPhotons){

    unsigned int n = camera->getN();
    unsigned int m = camera->getM();
    unsigned int mPerPixel = camera->getMPerPixel();
    unsigned int nPerPixel = camera->getNPerPixel();
    Mask mask(mPerPixel, nPerPixel);
    switch(camera->getFilterType())
    {
        case MEAN:
            mask.mean();
            break;
        case GAUSSIAN:
            mask.gaussian(camera->getFilterGaussianSigma());
            break;
        case LAPLACIAN:
            mask.laplacian(camera->getFilterLaplacianNeighborhood(), camera->getFilterLaplacianNegative());
            break;
        case WEIGHTED_AVERANGE:
            mask.weightedAverage();
            break;
    }
//    unsigned int q = camera->getNPerPixel();
//    unsigned int r = camera->getMPerPixel();

    for(unsigned int i = 0; i < n; i++)//altura
    {
        uchar *line = new uchar[3*m];

        _render->calculateLine(&line, i, m, nPerPixel, mPerPixel, &mask, _onlyPhotons);

        emit setLine(i, line);
        #if !USE_CONSOLE
//        #if DEBUG
        std::cout << ((i*100.0)/n) << "%   " << std::flush;
//        #endif //DEBUG
        #endif //USE_CONSOLE
    }
}

void Scene::testRaytracing(const Ray &_r){

    //faz o teste para todas as primitivas e desenha o raio
    listOfPoints.clear();
    if(objects.empty()) return;

    CombinedObject obj(UNION, objects, Material(), -1, true);

    listOfPoints = obj.intersection(_r);

    //inclui um ultimo ponto ao longo do raio, tendo o ultimo ponto base centro
    Vector3D end = listOfPoints.back().point;
    if(listOfPoints.size() == 1) listOfPoints.push_back(Trio(end + (_r.getDirection() * 10), Vector3D(1,0,0), OUT));
    else listOfPoints.push_back(Trio(end + _r.getDirection(), Vector3D(1,0,0), OUT));


}

void transformSecondToTime(int *_days, int *_hours, int *_minutes, int *_seconds, double _time)
{
    *_days = (int)(_time/(24.0*60.0*60.0));
    *_hours = ((int)(_time/(60.0*60.0)))%24;
    *_minutes = ((int)(_time/60))%60;
    *_seconds = ((int)_time)%60;
//    std::cout << "milissegundos  " << ((int)_time*1000)%60
//                << "microssegundo  " << ((int)_time*1000000)%60
//                << "? ------- " << _time<< std::endl;
}

void Scene::clearPhotonsMaps()
{
    photonMapping->clearMaps();
}
